﻿using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Linq;
using System.Diagnostics;
using System.Windows.Media;
using System.ComponentModel;
using System.Windows.Shapes;
using System.Windows.Input;
using System.Windows.Controls.DataVisualization.Charting;

namespace DemoZoomChart
{
    public enum ChartToolMode
    {
        ZoomBox,
        Pan
    }

    /// <summary>
    /// Charting sample that demonstrates zooming.
    /// </summary>
    public partial class SeriesZoomExtended : UserControl
    {
        private Panel chartPlotArea;
        private ScrollViewer chartScrollArea;
        private Border zoomBox;
        private Canvas zoomCanvas;

        private Panel xPlotArea;
        private ScrollViewer xScrollArea;
        private Panel yPlotArea;
        private ScrollViewer yScrollArea;
        private Point mouseStart;
        private ChartToolMode mode = ChartToolMode.Pan;
        
        private bool zoomBoxDrawing = false;
        private bool panning = false;
        private double zoomFactor = 1.0;
        private double maxZoomFactor = 10;
        private DateTime xMin;
        private DateTime xMax;
        private double yMin = 0.0;
        private double yMax = 0.0;

        #region Template Control Access

        private Panel ChartPlotArea
        {
            get
            {
                if (chartPlotArea == null)
                {
                    chartPlotArea = GetLogicalChildrenBreadthFirst(ZoomChart).Where(element => element.Name.Equals("ChartArea")).FirstOrDefault() as Panel;
                }

                return chartPlotArea;
            }
        }
      
        private ScrollViewer ChartScrollArea
        {
            get
            {
                if (chartScrollArea == null)
                {
                    chartScrollArea = GetLogicalChildrenBreadthFirst(ZoomChart).Where(element => element.Name.Equals("ChartScrollArea")).FirstOrDefault() as ScrollViewer;
                }
                return chartScrollArea;
            }
        }

        private Border ZoomBox
        {
            get
            {
                if (zoomBox == null)
                {
                    zoomBox = GetLogicalChildrenBreadthFirst(ZoomChart).Where(element => element.Name.Equals("ZoomBox")).FirstOrDefault() as Border;
                }
                return zoomBox;
            }
        }

        private Canvas ZoomCanvas
        {
            get
            {
                if (zoomBox == null)
                {
                    zoomCanvas = GetLogicalChildrenBreadthFirst(ZoomChart).Where(element => element.Name.Equals("ZoomCanvas")).FirstOrDefault() as Canvas;
                }
                return zoomCanvas;
            }
        }

        private Panel XPlotArea
        {
            get
            {
                if (xPlotArea == null)
                {
                    xPlotArea = GetLogicalChildrenBreadthFirst(ZoomXAxis).Where(element => element.Name.Equals("ChartArea")).FirstOrDefault() as Panel;
                }

                return xPlotArea;
            }
        }

        private ScrollViewer XScrollArea
        {
            get
            {
                if (xScrollArea == null)
                {
                    xScrollArea = GetLogicalChildrenBreadthFirst(ZoomXAxis).Where(element => element.Name.Equals("ChartScrollArea")).FirstOrDefault() as ScrollViewer;
                }
                return xScrollArea;
            }
        }

        private Panel YPlotArea
        {
            get
            {
                if (yPlotArea == null)
                {
                    yPlotArea = GetLogicalChildrenBreadthFirst(ZoomYAxis).Where(element => element.Name.Equals("ChartArea")).FirstOrDefault() as Panel;
                }

                return yPlotArea;
            }
        }

        private ScrollViewer YScrollArea
        {
            get
            {
                if (yScrollArea == null)
                {
                    yScrollArea = GetLogicalChildrenBreadthFirst(ZoomYAxis).Where(element => element.Name.Equals("ChartScrollArea")).FirstOrDefault() as ScrollViewer;
                }
                return yScrollArea;
            }
        }

        #endregion

        /// <summary>
        /// Initializes a new instance of the ZoomIntoChartSample class.
        /// </summary>
        public SeriesZoomExtended()
        {
            InitializeComponent();

            this.Loaded += ZoomIntoChartSample_Loaded;           
        }

        /// <summary>
        /// Force an update of the chart.
        /// </summary>
        /// <param name="sender">The ZoomIntoChartSample instance.</param>
        /// <param name="e">Event arguments.</param>
        private void ZoomIntoChartSample_Loaded(object sender, RoutedEventArgs e)
        {
            // Bind data
            GizmoPopularityPollCollection gizmos = new GizmoPopularityPollCollection();
            WidgetPopularityPollCollection widgets = new WidgetPopularityPollCollection();

            this.BindData(gizmos, widgets);

            this.ZoomChart.Focus();
        }

        // Manually attach mouse events
        private void ChartScrollArea_Loaded(object sender, RoutedEventArgs e)
        {            
            this.ChartPlotArea.MouseLeftButtonDown += this.ChartScrollArea_MouseLeftButtonDown;
            this.ChartPlotArea.MouseWheel += this.ChartScrollArea_MouseWheel;
        }

        // X Updated
        private void XScrollArea_LayoutUpdated(object sender, EventArgs e)
        {
            this.ChartScrollArea.ScrollToHorizontalOffset(this.XScrollArea.HorizontalOffset);
            this.ChartPlotArea.Width = this.XPlotArea.Width;
            this.ChartPlotArea.UpdateLayout();
            this.ChartScrollArea.UpdateLayout();
        }

        // Y Updated
        private void YScrollArea_LayoutUpdated(object sender, EventArgs e)
        {
            this.ChartScrollArea.ScrollToVerticalOffset(this.YScrollArea.VerticalOffset);
            this.ChartPlotArea.Height = this.YPlotArea.Height;
            this.ChartPlotArea.UpdateLayout();
            this.ChartScrollArea.UpdateLayout();
        }

        #region Chart Setup Methods

        public void BindData(GizmoPopularityPollCollection gizmos, WidgetPopularityPollCollection widgets)
        {
            // Bind series'
            LineSeries series1 = new LineSeries();
            series1.ItemsSource = gizmos;
            series1.Title="Gizmo Popularity";
            series1.IndependentValueBinding = new System.Windows.Data.Binding("Date");
            series1.DependentValueBinding = new System.Windows.Data.Binding("Percent");

            LineSeries series2 = new LineSeries();
            series2.ItemsSource = widgets;
            series2.Title = "Widget Popularity";
            series2.IndependentValueBinding = new System.Windows.Data.Binding("Date");
            series2.DependentValueBinding = new System.Windows.Data.Binding("Percent");

            this.ZoomChart.Series.Add(series1);
            this.ZoomChart.Series.Add(series2);

            this.ZoomChart.UpdateLayout();

            // Get Axis limits            
            DateTime minDate1 = gizmos.Min(d => d.Date);
            DateTime maxDate1 = gizmos.Max(d => d.Date);
            DateTime minDate2 = widgets.Min(d => d.Date);
            DateTime maxDate2 = widgets.Max(d => d.Date);

            this.xMin = minDate1 < minDate2 ? minDate1 : minDate2;
            this.xMax = maxDate1 > maxDate2 ? maxDate1 : maxDate2;

            // Size X
            (this.ZoomXAxis.Axes[0] as DateTimeAxis).Minimum = (this.ZoomChart.Axes[0] as DateTimeAxis).ActualMinimum;
            (this.ZoomXAxis.Axes[0] as DateTimeAxis).Maximum = (this.ZoomChart.Axes[0] as DateTimeAxis).ActualMaximum;

            // Size Y
            (this.ZoomYAxis.Axes[1] as LinearAxis).Minimum = (this.ZoomChart.Axes[1] as LinearAxis).ActualMinimum;
            (this.ZoomYAxis.Axes[1] as LinearAxis).Maximum = (this.ZoomChart.Axes[1] as LinearAxis).ActualMaximum;

            // Update Y Column
            this.UpdateYColumn();
        }

        private void UpdateYColumn()
        {
            this.ZoomYAxis.UpdateLayout();

            double width = 25.0d; // Scroller width

            width += (this.ZoomYAxis.Axes[1] as LinearAxis).ActualWidth;

            this.yColumn.Width = new GridLength(width);
            
        }

        #endregion

        #region Template helper methods

        private IEnumerable<FrameworkElement> GetLogicalChildrenBreadthFirst(FrameworkElement parent)
        {
            Debug.Assert(parent != null, "The parent cannot be null.");

            Queue<FrameworkElement> queue =
                new Queue<FrameworkElement>(GetVisualChildren(parent).OfType<FrameworkElement>());

            while (queue.Count > 0)
            {
                FrameworkElement element = queue.Dequeue();
                yield return element;

                foreach (FrameworkElement visualChild in GetVisualChildren(element).OfType<FrameworkElement>())
                {
                    queue.Enqueue(visualChild);
                }
            }
        }

        private IEnumerable<DependencyObject> GetVisualChildren(DependencyObject parent)
        {
            Debug.Assert(parent != null, "The parent cannot be null.");

            int childCount = VisualTreeHelper.GetChildrenCount(parent);
            for (int counter = 0; counter < childCount; counter++)
            {
                yield return VisualTreeHelper.GetChild(parent, counter);
            }
        }

        #endregion

        #region Mouse Events

        private void ChartScrollArea_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            this.mouseStart = e.GetPosition(this.ChartPlotArea);

            if (mode == ChartToolMode.Pan)
            {
                this.Cursor = Cursors.Hand;
                this.panning = true;                
            }

            if (mode == ChartToolMode.ZoomBox)
            {
                this.Cursor = Cursors.Stylus;
                zoomBoxDrawing = true;

                this.StartZoomBox(e.GetPosition(this.ZoomCanvas));
            }
        }        

        private void ChartScrollArea_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
        {
            if (mode == ChartToolMode.Pan && panning)
            {
                Point p = e.GetPosition(this.ChartPlotArea);
                this.Pan(p.X - this.mouseStart.X, p.Y - this.mouseStart.Y);
            }
            else if (mode == ChartToolMode.ZoomBox && zoomBoxDrawing)
            {
                this.MoveZoomBox(e.GetPosition(this.ZoomCanvas));
            }
        }

        private void ChartScrollArea_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            if (mode == ChartToolMode.ZoomBox && zoomBoxDrawing)
            {
                this.EndZoomBox(e.GetPosition(this.ChartPlotArea));                
            }

            this.zoomBoxDrawing = false;
            this.panning = false;
            this.Cursor = Cursors.Arrow;
        }

        private void ChartScrollArea_MouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
        {
            this. ZoomDelta(e.Delta, e.GetPosition(this.ChartPlotArea)) ; 
        }

        #endregion

        #region Toolbar Events

        private void btnPan_Click(object sender, RoutedEventArgs e)
        {
            this.Cursor = Cursors.Hand;
            this.mode = ChartToolMode.Pan;
        }

        private void btnZoom_Click(object sender, RoutedEventArgs e)
        {
            this.Cursor = Cursors.Stylus;
            this.mode = ChartToolMode.ZoomBox;
        }

        private void btnZoomCancel_Click(object sender, RoutedEventArgs e)
        {
            this.ResetZoom();
        }

        private void cboSpan_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {

            if (cboSpan != null && this.cboSpan.SelectedIndex > 0)
            {
                // Time spans
                TimeSpan tsSpan = new TimeSpan(int.Parse((this.cboSpan.SelectedItem as ComboBoxItem).Content.ToString()), 0, 0, 0);

                this.ChangeSpan(tsSpan);
            }
            else
            {
                this.ResetZoom();
            }
        }   

        #endregion

        #region Zoom methods

        private double ZoomToPoint_Zoom;
        private Point ZoomToPoint_Point;
        private bool ZoomToPoint_CallBackAdded = false;
        private bool ZoomToPoint_CallExpected = false;

        private void ZoomToPoint(double zoom, Point p)
        {
            this.ZoomToPoint_Zoom = zoom;
            this.ZoomToPoint_Point = p;
            this.ZoomToPoint_CallExpected = true;

            // Prevent callback being added multiple times
            if (!ZoomToPoint_CallBackAdded)
            {
                this.ZoomToPoint_CallBackAdded = true;

                // Fade out
                this.fadeChartOut.Completed += new EventHandler(delegate(object o, EventArgs e)
                    {
                        // Check this is the right event handler
                        if (this.ZoomToPoint_CallExpected)
                        {
                            this.ZoomToPoint_CallExpected = false;

                            this.zoomFactor *= this.ZoomToPoint_Zoom;

                            // X Width ZoomToPoint_Zoom
                            this.XPlotArea.Width = XPlotArea.ActualWidth * this.ZoomToPoint_Zoom;
                            this.XPlotArea.UpdateLayout();
                            this.XScrollArea.UpdateLayout();

                            // Y Height ZoomToPoint_Zoom
                            this.YPlotArea.Height = YPlotArea.ActualHeight * this.ZoomToPoint_Zoom;
                            this.YPlotArea.UpdateLayout();
                            this.YScrollArea.UpdateLayout();

                            // X Pan
                            double xPan = this.ZoomToPoint_Point.X * this.ZoomToPoint_Zoom - (XScrollArea.ViewportWidth / 2);
                            this.XScrollArea.ScrollToHorizontalOffset(xPan);

                            // Y Pan
                            double yPan = this.ZoomToPoint_Point.Y * this.ZoomToPoint_Zoom - (YScrollArea.ViewportHeight / 2);
                            this.YScrollArea.ScrollToVerticalOffset(yPan);

                            // Update Y Column
                            this.UpdateYColumn();

                            // Fade In
                            this.fadeChartIn.Begin();

                            return;
                        }
                    });
            }

            this.fadeChartOut.Begin();            
        }

        // Callback variables
        private double ZoomDelta_Zoom;
        private Point ZoomDelta_Point;
        private bool ZoomDelta_CallBackAdded = false;
        private bool ZoomDelta_CallExpected = false;

        private void ZoomDelta(double delta, Point p)
        {
            double zoom = 1;

            if (delta > 0 && zoomFactor * 1.2 <= maxZoomFactor)
            {
                zoom = 1.2;
            }
            else if (delta < 0 && zoomFactor * 0.8 >= 1)
            {
                zoom = 0.8;
            }
            else if (delta < 0)
            {
                this.ResetZoom();
                return;
            }

            // Callback variables
            this.ZoomDelta_Zoom = zoom;
            this.ZoomDelta_Point = p;
            this.ZoomDelta_CallExpected = true;

            // Prevent callback being added multiple times
            if (!ZoomDelta_CallBackAdded)
            {
                this.ZoomDelta_CallBackAdded = true;

                // Fade out
                this.fadeChartOut.Completed += new EventHandler(delegate(object o, EventArgs e)
                    {
                        // Check this is the right event handler
                        if (this.ZoomDelta_CallExpected)
                        {
                            this.ZoomDelta_CallExpected = false;
                            this.zoomFactor *= ZoomDelta_Zoom;

                            // X Margin from scroll edge
                            double xMargin = ZoomDelta_Point.X - this.XScrollArea.HorizontalOffset;

                            // Y Margin from scroll edge
                            double yMArgin = ZoomDelta_Point.Y - this.YScrollArea.VerticalOffset;

                            // X Width ZoomToPoint_Zoom
                            this.XPlotArea.Width = ZoomDelta_Zoom * this.XPlotArea.ActualWidth;
                            this.XPlotArea.UpdateLayout();
                            this.XScrollArea.UpdateLayout();

                            // Y Height ZoomToPoint_Zoom
                            this.YPlotArea.Height = ZoomDelta_Zoom * YPlotArea.ActualHeight;
                            this.YPlotArea.UpdateLayout();
                            this.YScrollArea.UpdateLayout();

                            // X Pan
                            double xPan = (ZoomDelta_Point.X * ZoomDelta_Zoom) - xMargin;
                            this.XScrollArea.ScrollToHorizontalOffset(xPan);

                            // Y Pan
                            double yPan = (ZoomDelta_Point.Y * ZoomDelta_Zoom) - yMArgin;
                            this.YScrollArea.ScrollToVerticalOffset(yPan);

                            // Fade In
                            this.fadeChartIn.Begin();

                            return;
                        }
                    });
            }

            this.fadeChartOut.Begin(); 
        }


        private void StartZoomBox(Point zoomPoint)
        {
            // Draw ZoomToPoint_Zoom box at point
            ZoomBox.Height = 0.0;
            ZoomBox.Width = 0.0;
            ZoomBox.SetValue(Canvas.TopProperty, zoomPoint.Y);
            ZoomBox.SetValue(Canvas.LeftProperty, zoomPoint.X);
            ZoomBox.Visibility = System.Windows.Visibility.Visible;
        }

        private void MoveZoomBox(Point zoomPoint)
        {
            double left = (double)ZoomBox.GetValue(Canvas.LeftProperty);
            double top = (double)ZoomBox.GetValue(Canvas.TopProperty);

            // Resize Repostition if required
            if (zoomPoint.X > left)
            {
                ZoomBox.Width = zoomPoint.X - left;
            }
            else
            {
                ZoomBox.Width += left - zoomPoint.X;
                ZoomBox.SetValue(Canvas.LeftProperty, zoomPoint.X);
            }

            if (zoomPoint.Y > top)
            {
                ZoomBox.Height = zoomPoint.Y - top;
            }
            else
            {
                ZoomBox.Height += top - zoomPoint.Y;
                ZoomBox.SetValue(Canvas.TopProperty, zoomPoint.Y);
            }
        }

        private void EndZoomBox(Point zoomPoint)
        {
            // Zoom box top and left relative to chart
            double left = mouseStart.X < zoomPoint.X ? mouseStart.X : zoomPoint.X;
            double top = mouseStart.Y < zoomPoint.Y ? mouseStart.Y : zoomPoint.Y;

            // Zoom box centres
            double centreX = left + (this.ZoomBox.Width / 2);
            double centreY = top + (this.ZoomBox.Height / 2);

            // Chart area must be zoomed to allow ZoomToPoint_Zoom box to fill scroll viewer
            double xFactor = this.ChartScrollArea.ViewportWidth / this.ZoomBox.Width;
            double yFactor = this.ChartScrollArea.ViewportHeight / this.ZoomBox.Height;

            // Zoom to smallest factor
            double zoom = xFactor < yFactor ? xFactor : yFactor;

            // Check ZoomToPoint_Zoom bounds
            if (zoom * this.zoomFactor >= 1 && zoom * this.zoomFactor < this.maxZoomFactor)
            {
                // Centres
                Point p = new Point(centreX, centreY);

                // Zoom
                this.ZoomToPoint(zoom, p);
            }

            this.ZoomBox.Visibility = System.Windows.Visibility.Collapsed;
        }

        // Callback variables
        private double ChangeSpan_Zoom;
        private bool ChangeSpan_CallBackAdded = false;
        private bool ChangeSpan_CallExpected = false;

        // Change chart span
        private void ChangeSpan(TimeSpan tsSpan)
        {
            // Time spans
            TimeSpan tsX = xMax - xMin;

            // Zoom factor
            double zoom = (tsX.Ticks / tsSpan.Ticks);
            if (zoom == 0) zoom = 1.0d;

            // Callback variables
            this.ChangeSpan_Zoom = zoom;
            this.ChangeSpan_CallExpected = true;

            // Prevent callback being added multiple times
            if (!ChangeSpan_CallBackAdded)
            {
                this.ChangeSpan_CallBackAdded = true;

                // Fade out
                this.fadeChartOut.Completed += new EventHandler(delegate(object o, EventArgs e)
                    {
                        // Check this is the right event handler
                        if (this.ChangeSpan_CallExpected)
                        {
                            this.ChangeSpan_CallExpected = false;

                            // X Width ZoomToPoint_Zoom
                            this.XPlotArea.Width = ChartScrollArea.ViewportWidth * ChangeSpan_Zoom;
                            this.XPlotArea.UpdateLayout();
                            this.XScrollArea.UpdateLayout();

                            // Y Height ZoomToPoint_Zoom
                            this.YPlotArea.Height = ChartScrollArea.ViewportHeight * 1;
                            this.YPlotArea.UpdateLayout();
                            this.YScrollArea.UpdateLayout();

                            // Update Y Column
                            this.UpdateYColumn();

                            // Fade In
                            this.fadeChartIn.Begin();

                            return;
                        }
                    });
            }

            this.fadeChartOut.Begin(); 
        }

        // Callback variables
        private bool ResetZoom_CallbackAdded = false;
        private bool ResetZoom_CallbackExpected = false;

        private void ResetZoom()
        {
            this.ResetZoom_CallbackExpected = true;
            this.zoomFactor = 1.0;

            // Fade chart out
            if (!ResetZoom_CallbackAdded)
            {
                ResetZoom_CallbackAdded = true;

                this.fadeChartOut.Completed += new EventHandler(delegate(object o, EventArgs e)
                    {
                        // Check this is the right event handler
                        if (this.ResetZoom_CallbackExpected)
                        {
                            this.ResetZoom_CallbackExpected = false;

                            // X Width ZoomToPoint_Zoom
                            this.XPlotArea.Width = XScrollArea.ViewportWidth;
                            this.XPlotArea.UpdateLayout();
                            this.XScrollArea.UpdateLayout();

                            // Y Height ZoomToPoint_Zoom
                            this.YPlotArea.Height = YScrollArea.ViewportHeight;
                            this.YPlotArea.UpdateLayout();
                            this.YScrollArea.UpdateLayout();

                            // Fade chart in
                            this.fadeChartIn.Begin();

                            return;
                        }
                    });
            }

            this.fadeChartOut.Begin();
        }

        #endregion

        #region Pan

        private void Pan(double x, double y)
        {           
            // X Pan
            this.XScrollArea.ScrollToHorizontalOffset(ChartScrollArea.HorizontalOffset - x);

            // Y Pan
            this.YScrollArea.ScrollToVerticalOffset(ChartScrollArea.VerticalOffset - y);            
        }

        #endregion

        
               
    }
}